﻿using OpenTK;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LDLib
{
    public class Camera
    {
        public static Camera Main { get; set; }

        /// <summary>
        /// View matrix
        /// </summary>
        public Matrix4 modelview;

        /// <summary>
        /// Projection matrix
        /// </summary>
        public Matrix4 projection;

        /// <summary>
        /// Orthogonal size of the view in orthogonal projection
        /// </summary>
        public float orthoSize = 45f;

        /// <summary>
        /// Field of view in perspective projection
        /// </summary>
        public float perspectiveFov = 60f;

        /// <summary>
        /// Near plane
        /// </summary>
        public float nearZ = 5f;

        /// <summary>
        /// Far plane (draw distance)
        /// </summary>
        public float farZ = 500f;

        private float[] frustum = new float[24];

        /// <summary>
        /// Transform of this camera
        /// </summary>
        public Transform transform = new Transform();

        private bool isPerspective = true;

        public bool IsPerspective { get { return isPerspective; } }

        /// <summary>
        /// Constructs and retrieves the view matrix
        /// </summary>
        public Matrix4 View
        {
            get
            {
                Matrix4 tmp = Matrix4.Rotate(transform.Rotation) * Matrix4.CreateTranslation(transform.Location);
                return tmp;
            }
        }

        /// <summary>
        /// Constructs and retrieves the inverse view matrix
        /// </summary>
        public Matrix4 InverseView
        {
            get
            {
                Matrix4 tmp = View;
                tmp.Invert();
                return tmp;
            }
        }

        /// <summary>
        /// Constructs a new camera with default view and projection matrices
        /// </summary>
        public Camera()
        {
            // If the main reference is null, set this new camera as the main camera
            if (Camera.Main == null) SetMain();

            // Reset the matrices to default settings
            ResetCamera();
        }

        /// <summary>
        /// Resets the default values for the camera
        /// </summary>
        public void ResetCamera()
        {
            transform.Rotation = Quaternion.Identity;
            transform.Location = new Vector3(0f, 0f, 30f);
            orthoSize = Properties.ScreenHeight / 16f;
            ChangeView(isPerspective);
        }

        /// <summary>
        /// Sets this camera as the main camera
        /// </summary>
        public void SetMain()
        {
            Camera.Main = this;
        }

        public void Update()
        {
            UpdateFrustum();
        }

        /// <summary>
        /// Updates the view frustum
        /// </summary>
        public void UpdateFrustum()
        {
            Matrix4 mat = InverseView * projection;
            float t;

            // Right plane
            frustum[0] = mat.M14 - mat.M11;
            frustum[1] = mat.M24 - mat.M21;
            frustum[2] = mat.M34 - mat.M31;
            frustum[3] = mat.M44 - mat.M41;
            t = (float)Math.Sqrt(frustum[0] * frustum[0] + frustum[1] * frustum[1] + frustum[2] * frustum[2]);
            frustum[0] /= t;
            frustum[1] /= t;
            frustum[2] /= t;
            frustum[3] /= t;

            // Left plane
            frustum[4] = mat.M14 + mat.M11;
            frustum[5] = mat.M24 + mat.M21;
            frustum[6] = mat.M34 + mat.M31;
            frustum[7] = mat.M44 + mat.M41;
            t = (float)Math.Sqrt(frustum[4] * frustum[4] + frustum[5] * frustum[5] + frustum[6] * frustum[6]);
            frustum[4] /= t;
            frustum[5] /= t;
            frustum[6] /= t;
            frustum[7] /= t;

            // Bottom plane
            frustum[8] = mat.M14 + mat.M12;
            frustum[9] = mat.M24 + mat.M22;
            frustum[10] = mat.M34 + mat.M32;
            frustum[11] = mat.M44 + mat.M42;
            t = (float)Math.Sqrt(frustum[8] * frustum[8] + frustum[9] * frustum[9] + frustum[10] * frustum[10]);
            frustum[8] /= t;
            frustum[9] /= t;
            frustum[10] /= t;
            frustum[11] /= t;

            // Top plane
            frustum[12] = mat.M14 - mat.M12;
            frustum[13] = mat.M24 - mat.M22;
            frustum[14] = mat.M34 - mat.M32;
            frustum[15] = mat.M44 - mat.M42;
            t = (float)Math.Sqrt(frustum[12] * frustum[12] + frustum[13] * frustum[13] + frustum[14] * frustum[14]);
            frustum[12] /= t;
            frustum[13] /= t;
            frustum[14] /= t;
            frustum[15] /= t;

            // Far plane
            frustum[16] = mat.M14 - mat.M13;
            frustum[17] = mat.M24 - mat.M23;
            frustum[18] = mat.M34 - mat.M33;
            frustum[19] = mat.M44 - mat.M43;
            t = (float)Math.Sqrt(frustum[16] * frustum[16] + frustum[17] * frustum[17] + frustum[18] * frustum[18]);
            frustum[16] /= t;
            frustum[17] /= t;
            frustum[18] /= t;
            frustum[19] /= t;

            // Near plane
            frustum[20] = mat.M14 + mat.M13;
            frustum[21] = mat.M24 + mat.M23;
            frustum[22] = mat.M34 + mat.M33;
            frustum[23] = mat.M44 + mat.M43;
            t = (float)Math.Sqrt(frustum[20] * frustum[20] + frustum[21] * frustum[21] + frustum[22] * frustum[22]);
            frustum[20] /= t;
            frustum[21] /= t;
            frustum[22] /= t;
            frustum[23] /= t;
        }

        /// <summary>
        /// Checks if a point is inside the view frustum
        /// </summary>
        /// <param name="p">Point</param>
        /// <returns>True if the point is inside the view frustum, false otherwise</returns>
        public bool PointInFrustum(Vector3 p)
        {
            for (int i = 0; i < 6; ++i)
            {
                if (frustum[i * 4] * p.X + frustum[i * 4 + 1] * p.Y + frustum[i * 4 + 2] * p.Z + frustum[i * 4 + 3] <= 0f)
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Checks if a sphere is inside the view frustum
        /// </summary>
        /// <param name="p">Location of the sphere</param>
        /// <param name="radius">Radius of the sphere</param>
        /// <returns>True if the sphere is inside the view frustum, false otherwise</returns>
        public bool SphereInFrustum(Vector3 p, float radius)
        {
            if (frustum[0] * p.X + frustum[1] * p.Y + frustum[2] * p.Z + frustum[3] <= -radius ||
                frustum[4] * p.X + frustum[5] * p.Y + frustum[6] * p.Z + frustum[7] <= -radius ||
                frustum[8] * p.X + frustum[9] * p.Y + frustum[10] * p.Z + frustum[11] <= -radius ||
                frustum[12] * p.X + frustum[13] * p.Y + frustum[14] * p.Z + frustum[15] <= -radius ||
                frustum[16] * p.X + frustum[17] * p.Y + frustum[18] * p.Z + frustum[19] <= -radius ||
                frustum[20] * p.X + frustum[21] * p.Y + frustum[22] * p.Z + frustum[23] <= -radius)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Changes the projection between orthographic and perspective projection modes
        /// </summary>
        /// <param name="isPerspective">If 'true', the camera will use projection matrix. If 'false', the camera will use orthogonal matrix.</param>
        public void ChangeView(bool isPerspective)
        {
            this.isPerspective = isPerspective;
            if (isPerspective)
            {
                projection = Matrix4.CreatePerspectiveFieldOfView(MathHelper.DegreesToRadians(45f), Properties.ScreenRatio, nearZ, farZ);
            }
            else
            {
                projection = Matrix4.CreateOrthographic(Properties.ScreenRatio * orthoSize, orthoSize, nearZ, farZ);
            }
        }

        /// <summary>
        /// Creates a ray from a screen point
        /// </summary>
        /// <param name="x">X coordinate on the screen</param>
        /// <param name="y">Y coordinate on the screen</param>
        /// <param name="origin">Origin of the ray</param>
        /// <param name="direction">Direction of the ray</param>
        public void ScreenPointToRay(int x, int y, out Vector3 origin, out Vector3 direction)
        {
            Size size = new Size(Properties.ScreenWidth, Properties.ScreenHeight);
            Vector4 unProjNear = MathTools.UnProject(Camera.Main.projection, Camera.Main.InverseView, size, new Vector3(x, y, 0f));
            Vector4 unProjFar = MathTools.UnProject(Camera.Main.projection, Camera.Main.InverseView, size, new Vector3(x, y, 1f));

            Vector4 ray = unProjFar - unProjNear;
            ray.Normalize();

            origin = new Vector3(unProjNear);
            direction = new Vector3(ray);
        }

        /// <summary>
        /// Converts a world position to screen point
        /// </summary>
        /// <param name="position">Position in world</param>
        /// <param name="screenPos">Screen position</param>
        public void WorldToScreenPoint(Vector3 position, out Vector2 screenPos)
        {
            Vector4 r = Vector4.Transform(new Vector4(position, 1f), InverseView);
            r = Vector4.Transform(r, projection);
            screenPos = new Vector2(r.X * 0.5f / r.W + 0.5f, r.Y * 0.5f / r.W + 0.5f);
            screenPos.X *= Properties.ScreenWidth;
            screenPos.Y *= Properties.ScreenHeight;
        }
    }
}
